| 版本 | 日期 | 更新内容 | 作者 |
|---|---|---|---|
| 2.0.0 | 2024-12-04 | 文档结构优化、内容完善、格式规范化 | AI Assistant |
| 1.0.0 | 2024-XX-XX | 初始版本 | QuarkGluonPlasma |
本章节将详细介绍如何在脚手架 CLI 工具中实现 NPM 包的版本号获取和下载功能。这是实现 create 命令的关键步骤,需要从 NPM 仓库获取模板包并支持版本更新。
在继续实现 create 命令之前,需要先创建一个工具包来封装 NPM 包相关的操作逻辑。
packages/
├── utils/ # 工具包
│ ├── src/
│ │ ├── versionUtils.ts # 版本号获取工具
│ │ ├── NpmPackage.ts # NPM 包管理类
│ │ └── index.ts # 导出模块
│ ├── package.json
│ └── tsconfig.json
├── cli/ # CLI 主包
└── core/ # 核心逻辑包首先创建 utils 包并初始化:
# 创建 utils 包目录
mkdir packages/utils
cd packages/utils
# 初始化 package.json
npm init -y修改生成的 package.json 文件,添加必要的配置:
{
"name": "@guang-cli/utils",
"version": "1.0.0",
"description": "CLI 工具函数集合",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": ["dist"],
"scripts": {
"build": "tsc",
"dev": "tsc --watch"
},
"publishConfig": {
"access": "public"
},
"keywords": ["cli", "utils", "npm", "scaffold"]
}配置说明:
"type": "module":启用 ES Modules 支持"main" 和 "types":指定编译后的入口文件和类型声明文件"publishConfig.access": "public":设置为公开包,允许发布到 NPM初始化 TypeScript 配置:
# 使用 pnpm filter 在 utils 包内执行命令
pnpm --filter utils exec npx tsc --init修改 tsconfig.json:
{
"compilerOptions": {
"outDir": "dist",
"types": ["node"],
"target": "es2016",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"sourceMap": true,
"removeComments": false,
"noImplicitAny": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}安装 HTTP 请求和 URL 处理相关的依赖:
pnpm --filter utils add axios url-join
pnpm --filter utils add -D @types/node创建 src/versionUtils.ts 文件:
import axios from "axios"
import urlJoin from "url-join"
/**
* 获取 NPM Registry 地址
* @returns NPM Registry URL
*/
export function getNpmRegistry(): string {
return "https://registry.npmmirror.com"
}
/**
* 获取 NPM 包信息
* @param packageName - 包名
* @returns NPM 包信息对象
* @throws 当请求失败时抛出错误
*/
export async function getNpmInfo(packageName: string): Promise<any> {
const registry = getNpmRegistry()
const url = urlJoin(registry, packageName)
try {
const response = await axios.get(url, {
timeout: 10000, // 10秒超时
headers: {
"User-Agent": "@guang-cli/utils"
}
})
if (response.status === 200) {
return response.data
}
throw new Error(`Failed to fetch package info: ${response.status}`)
} catch (error) {
if (axios.isAxiosError(error)) {
throw new Error(`Network error: ${error.message}`)
}
throw error
}
}
/**
* 获取最新版本号
* @param packageName - 包名
* @returns 最新版本号
*/
export async function getLatestVersion(packageName: string): Promise<string> {
const data = await getNpmInfo(packageName)
if (!data["dist-tags"]?.latest) {
throw new Error(`Package ${packageName} has no latest version`)
}
return data["dist-tags"].latest
}
/**
* 获取所有版本号列表
* @param packageName - 包名
* @returns 版本号数组,按发布时间排序
*/
export async function getVersions(packageName: string): Promise<string[]> {
const data = await getNpmInfo(packageName)
if (!data.versions) {
throw new Error(`Package ${packageName} has no versions`)
}
return Object.keys(data.versions)
}https://registry.npmmirror.com)packageName - NPM 包名dist-tags.latest 字段获取"1.2.3"versions 对象获取所有键["1.0.0", "1.1.0", "2.0.0"]创建测试文件 src/test.ts:
import { getNpmInfo, getLatestVersion, getVersions } from "./versionUtils.js"
async function testVersionUtils() {
console.log("🧪 测试版本工具函数...\n")
try {
// 测试获取包信息
console.log("📦 获取 create-vite 包信息:")
const info = await getNpmInfo("create-vite")
console.log(`包名: ${info.name}`)
console.log(`最新版本: ${info["dist-tags"]?.latest}`)
console.log(`总版本数: ${Object.keys(info.versions || {}).length}`)
console.log("\n🏷️ 获取最新版本:")
const latestVersion = await getLatestVersion("create-vite")
console.log(`create-vite 最新版本: ${latestVersion}`)
console.log("\n📋 获取所有版本:")
const versions = await getVersions("create-vite")
console.log(`create-vite 最近 5 个版本: ${versions.slice(-5).join(", ")}`)
} catch (error) {
console.error("❌ 测试失败:", error instanceof Error ? error.message : error)
}
}
testVersionUtils()运行测试:
# 编译 TypeScript
pnpm --filter utils exec npx tsc
# 运行测试
pnpm --filter utils exec node ./dist/test.js安装文件系统操作相关的依赖:
pnpm --filter utils add fs-extra npminstall
pnpm --filter utils add -D @types/fs-extra创建 src/NpmPackage.ts:
import fs from "node:fs"
import path from "node:path"
import fse from "fs-extra"
// @ts-ignore - npminstall 没有 TypeScript 类型定义
import npminstall from "npminstall"
import { getLatestVersion, getNpmRegistry } from "./versionUtils.js"
export interface NpmPackageOptions {
name: string
targetPath: string
}
/**
* NPM 包管理类
* 提供包的安装、更新、检查存在等功能
*/
export class NpmPackage {
public readonly name: string
public readonly targetPath: string
public readonly storePath: string
private version: string = ""
constructor(options: NpmPackageOptions) {
this.name = options.name
this.targetPath = options.targetPath
this.storePath = path.resolve(options.targetPath, "node_modules")
}
/**
* 准备工作:创建目标目录,获取最新版本号
*/
private async prepare(): Promise<void> {
// 确保目标目录存在
if (!fs.existsSync(this.targetPath)) {
await fse.ensureDir(this.targetPath)
}
// 如果还没有版本号,获取最新版本
if (!this.version) {
this.version = await getLatestVersion(this.name)
}
}
/**
* 安装包
* @returns 安装结果
*/
async install(): Promise<any> {
await this.prepare()
return npminstall({
pkgs: [
{
name: this.name,
version: this.version
}
],
registry: getNpmRegistry(),
root: this.targetPath,
// 其他配置选项
cacheDir: path.join(this.targetPath, ".npm_cache"),
timeout: 60000 // 60秒超时
})
}
/**
* 获取包在本地存储的路径
* 处理 @scope/package 格式的包名
*/
get npmFilePath(): string {
const packageName = this.name.replace("/", "+")
return path.resolve(
this.storePath,
`.store/${packageName}@${this.version}/node_modules/${this.name}`
)
}
/**
* 检查包是否已安装
* @returns 是否已安装
*/
async exists(): Promise<boolean> {
await this.prepare()
return fs.existsSync(this.npmFilePath)
}
/**
* 获取包的 package.json 内容
* @returns package.json 对象,如果包不存在则返回 null
*/
async getPackageJSON(): Promise<any | null> {
if (await this.exists()) {
const packageJsonPath = path.resolve(this.npmFilePath, "package.json")
return await fse.readJson(packageJsonPath)
}
return null
}
/**
* 获取最新版本号
* @returns 最新版本号
*/
async getLatestVersion(): Promise<string> {
return getLatestVersion(this.name)
}
/**
* 更新包到最新版本
* @returns 更新结果
*/
async update(): Promise<any> {
const latestVersion = await this.getLatestVersion()
// 如果已经是最新版本,不需要更新
if (latestVersion === this.version) {
return { updated: false, version: this.version }
}
this.version = latestVersion
return npminstall({
root: this.targetPath,
registry: getNpmRegistry(),
pkgs: [
{
name: this.name,
version: latestVersion
}
],
cacheDir: path.join(this.targetPath, ".npm_cache"),
timeout: 60000
})
}
/**
* 获取当前版本号
* @returns 当前版本号
*/
get currentVersion(): string {
return this.version
}
}
export default NpmPackagename: NPM 包名targetPath: 目标安装路径storePath/ 替换为 +(处理 scoped packages).store/package@version/node_modules/packagebooleannpmFilePath 是否存在null创建测试文件:
import NpmPackage from "./NpmPackage.js"
import path from "node:path"
import { fileURLToPath } from "node:url"
const __dirname = path.dirname(fileURLToPath(import.meta.url))
async function testNpmPackage() {
console.log("🧪 测试 NPM 包管理类...\n")
const testPath = path.join(__dirname, "../test-packages")
// 测试 create-vite 包
const pkg = new NpmPackage({
targetPath: testPath,
name: "create-vite"
})
try {
// 检查包是否存在
const exists = await pkg.exists()
console.log(`📦 create-vite 已安装: ${exists}`)
if (exists) {
// 如果已安装,获取包信息
const packageJson = await pkg.getPackageJSON()
console.log(`当前版本: ${packageJson?.version}`)
// 更新到最新版本
console.log("🔄 更新到最新版本...")
const updateResult = await pkg.update()
console.log("更新完成")
} else {
// 如果未安装,进行安装
console.log("📥 安装 create-vite...")
await pkg.install()
console.log("安装完成")
}
// 获取最新版本信息
const latestVersion = await pkg.getLatestVersion()
console.log(`最新版本: ${latestVersion}`)
} catch (error) {
console.error("❌ 测试失败:", error instanceof Error ? error.message : error)
}
}
testNpmPackage()创建 src/index.ts 统一导出模块:
/**
* @guang-cli/utils - CLI 工具函数集合
*
* 提供以下功能:
* - NPM 包版本号获取
* - NPM 包安装和管理
* - 包存在性检查
* - 包更新功能
*/
import NpmPackage from "./NpmPackage.js"
import * as versionUtils from "./versionUtils.js"
export { NpmPackage, versionUtils }
export type { NpmPackageOptions } from "./NpmPackage.js"
// 默认导出
export default {
NpmPackage,
versionUtils
}在发布之前,先提交本地代码:
# 添加所有更改
git add .
# 提交更改
git commit -m "feat(utils): 添加 NPM 包版本获取和下载功能"# 创建新的 changeset
npx changeset add执行命令后,会进入交互式界面:
@guang-cli/utilspatch: 修复 Bug,版本号第三位 +1minor: 新增功能,版本号第二位 +1major: 破坏性更新,版本号第一位 +1# 根据 changeset 生成新版本号和更新日志
npx changeset version这个命令会:
package.json 中的版本号CHANGELOG.md 文件.changeset 目录下的临时文件# 构建项目
pnpm --filter utils build
# 发布到 NPM
npx changeset publish发布成功后,changeset 会自动:
发布完成后,可以通过以下方式验证:
npm install @guang-cli/utilsnpm view @guang-cli/utilsA: npminstall 是淘宝团队开发的 NPM 包安装工具,具有以下优势:
A: 在计算文件路径时,需要将包名中的 / 替换为 +,例如:
@babel/core → @babel+core.store/@babel+core@7.0.0/node_modules/@babel/coreA: 建议的处理流程:
完成 NPM 包版本获取和下载功能后,可以继续实现:
本章节完成了以下工作:
✅ 创建了 utils 工具包,封装了 NPM 包相关的操作
✅ 实现了版本号获取功能,包括:
✅ 开发了 NPM 包管理类,支持:
✅ 发布了工具包到 NPM,使用 changeset 进行版本管理
这些功能为后续实现 create 命令奠定了基础,使得脚手架工具能够从 NPM 仓库下载模板包并管理版本更新。